#!/usr/bin/python -S
"""
cmdapp.py

Framework for command line tools, analagous to webapp.py.

# TODO: unify with webapp.
"""

__author__ = 'Andy Chu'


import getopt
import re
import sys

import params
import records

Record = records.Record  # breaking the rules a bit


def _ShowHelp(param_definition):
  """Show a help string for the parameter definition.

  TODO: Show all info, positional args, etc.
  """
  print
  for param in param_definition:

    if param.is_required:
      prefix = '* '
    else:
      prefix = '  '

    long_name = '--%s' % param.name
    if param.shortcut:
      print '%s%-60s  (-%s)' % (prefix, long_name, param.shortcut)
    else:
      print prefix + long_name

    if param.help:
      lines = param.help.splitlines()
      # Indent it.  TODO: Need some sort of paragraph wrap.
      for line in lines:
        print '    ' + line
    else:
      # TODO: Point to a place where people can contribute docs
      print '    (no help)'

    print

  print '* Required parameters are marked with an asterisk.'


def _ShortDef(param):
  if param.type == 'boolean':
    return param.shortcut
  else:
    return param.shortcut + ':'


def _LongDef(param):
  if param.type == 'boolean':
    # --no-foo turns param 'foo' off
    return [param.name, 'no-' + param.name]
  else:
    return [param.name + '=']


def ParseArgv(argv, param_definition, do_help=True):
  """Parse arguments.

  This is
  Args:
    do_help: Whether to parse and show help
  """
  # TODO: There's probably a more principled way to do this.  Should be able to
  # get help when running with query params.  But this is useful for now.
  if argv == ['-h'] or argv == ['--help']:
    _ShowHelp(param_definition)
    sys.exit(0)  # BAD

  raw_params = ArgvToRawParams(argv, param_definition)
  return params.MakeRecord(raw_params, param_definition)


def ArgvToRawParams(argv, param_definition):
  """
  This is public since processor_base.Processor uses it

  Converts --foo-bar 3 -> {"foo-bar": "3"}  (no type conversion yet)
  """
  # TODO: Need more error checking here, more tests

  short_opts = ''.join(_ShortDef(p) for p in param_definition if p.shortcut)
  long_opts = []
  for p in param_definition:
    long_opts.extend(_LongDef(p))

  try:
    # TODO: check that GNU style is the one we want
    pairs, argv = getopt.gnu_getopt(argv, short_opts, long_opts)
  except getopt.GetoptError, e:
    raise params.InvalidInput(str(e))

  raw_params = _ParsePositionalArgs(argv, param_definition)

  for name, value in pairs:

    if name.startswith('--no-'):  # Turn off a boolean
      for p in param_definition:
        if p.type == 'boolean' and p.name == name[5:]:
          raw_params[p.name] = False

    if name.startswith('--'):
      for p in param_definition:
        if p.name == name[2:]:
          if p.type == 'boolean':
            value = True
          raw_params[p.name] = value

    elif name.startswith('-'):
      for p in param_definition:
        if p.shortcut == name[1:]:
          if p.type == 'boolean':
            value = True
          raw_params[p.name] = value

    else:
      raise AssertionError(name)

  return raw_params


def _ParsePositionalArgs(argv, param_definition):
  # Try to match positional arguments

  lookup = [None] + argv  # simulate 1-based indexing
  used = [False] * len(argv)  # index matches argv, not lookup

  raw_params = {}

  for i, p in enumerate(param_definition):
    if hasattr(p, 'pos') and p.pos and type(p.pos) is int:
      try:
        value = lookup[p.pos]
      except IndexError:
        if p.is_required:
          raise params.InvalidInput(
              'Argument at position %s not specified' % p.pos)
      else:
        raw_params[p.name] = value
        used[p.pos - 1] = True

  def _IsRepeated(pos):
    if type(pos) is str:
      # Right now we don't need any more distinction between 1+, 2+, etc.  They
      # are mainly there for readability purposes now.
      assert re.match(r'\d\+', p.pos)
      return True
    else:
      return False

  # Fill in variable arguments
  for p in param_definition:
    if hasattr(p, 'pos') and _IsRepeated(p.pos):
      unused_argv = []
      for (i, arg) in enumerate(argv):
        if not used[i]:
          unused_argv.append(arg)
      raw_params[p.name] = unused_argv

  return raw_params


class CmdApp(object):
  """A wrapper for Processors.

  Analagous to webapp.WebApp.  That is a WSGI app that runs various
  webapp.Resources.

  This is a "PCLI" app that runs various Processors.
  """
  def __init__(self, proc_class):
    """
    Args:
      processor_base.Processor class.
    """
    self._proc_class = proc_class
    self.exit_code = None

  def __call__(self, environ):
    self.environ = environ
    return self

  def __iter__(self):
    argv = self.environ['argv']

    # TODO: catch exceptions here, and call exit_func?
    # argv[0] is the executable, so we don't need it.
    processor = self._proc_class.FromArgv(argv[1:])
    if processor.NeedsInput():
      processor.AttachInput(self.environ['pcli.input'])

    # TODO: stderr
    # TODO: JSON marshaling, unmarshalling?

    for chunk in processor:
      yield chunk
    self.exit_code = 0


class SimpleHandler(object):
  """
  PCLI is Pan Command Line Interface, a cheesy name to emphasize the analogy
  with WSGI.

  The idea is that a command line program can be run in the normal way, with
  sys.stdout, stdin, stderr; or it can be run as a web app, or in process with
  "fake" file handles, etc.

  There are no exceptions in this interface, as there are none in WSGI.
  """
  def __init__(self, argv=sys.argv, stdin=sys.stdin, stdout=sys.stdout,
      stderr=sys.stdout, exit_func=sys.exit):
    self._argv = argv
    self._stdin = stdin
    self._stdout = stdout
    self._stderr = stderr
    self._exit_func = exit_func

  def run(self, app):
    environ = {
        'pcli.input': self._stdin,
        'pcli.error_stream': self._stderr,
        'argv': self._argv,
        }
    app_instance = app(environ)
    for chunk in app_instance:
      self._stdout.write(chunk)
    # The result is only defined after the output has all been consumed.
    self._exit_func(app_instance.exit_code)

